package edu.hawaii.myisern.xmldatabase;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

import org.xml.sax.SAXException;

import edu.hawaii.myisern.collaborations.jaxb.Collaboration;
import edu.hawaii.myisern.collaborations.jaxb.Collaborations;
import edu.hawaii.myisern.organizations.jaxb.Organization;
import edu.hawaii.myisern.organizations.jaxb.Organizations;
import edu.hawaii.myisern.researchers.jaxb.Researcher;
import edu.hawaii.myisern.researchers.jaxb.Researchers;

/**
 * Load instances from XML files, validate them and answer for queries.
 * 
 * @author Team MyISERN-1-Ivory
 * 
 */
public class XmlLoader {
  /** Holds the String of USER_DIR. */
  private static final String USER_DIR = "user.dir";
  /** Holds the default XML file name of Collaborations. */
  public static final String COLLABORATIONS_XML = "collaborations.xml";
  /** Holds the default XML file name of Organizations. */
  public static final String ORGANIZATIONS_XML = "organizations.xml";
  /** Holds the default XML file name of Researchers. */
  public static final String RESEARCHERS_XML = "researchers.xml";
  /** Holds the path of Collaborations XML file. */
  public String collaborationsXml;
  /** Holds the path of Organizations XML file. */
  public String organizationsXml;
  /** Holds the path of Researchers XML file. */
  public String researchersXml;
  /** Holds the path of Collaborations XSD file. */
  /*
  private final String COLLABORATIONS_XSD = System.getProperty(USER_DIR)
      + "/xml/schema/collaborations.xsd";
  */
  /** Holds the path of Organizations XSD file. */
  /*
  private final String ORGANIZATIONS_XSD = System.getProperty(USER_DIR)
      + "/xml/schema/organizations.xsd";
  */
  /** Holds the path of Researchers XSD file. */
  /*
  private final String RESEARCHERS_XSD = System.getProperty(USER_DIR)
      + "/xml/schema/researchers.xsd";
  */
  /** Holds the class-wide JAXBContext, which is thread-safe. */
  private JAXBContext collaborationsJaxbContext;
  /** Holds the class-wide JAXBContext, which is thread-safe. */
  private JAXBContext organizationsJaxbContext;
  /** Holds the class-wide JAXBContext, which is thread-safe. */
  private JAXBContext researchersJaxbContext;

  /** Holds the Collaborations object that retrieved from XML file. */
  private Collaborations collaborations;
  /** Holds the Collaborations object that retrieved from XML file. */
  private Organizations organizations;
  /** Holds the Collaborations object that retrieved from XML file. */
  private Researchers researchers;

  /** Holds the global Unique ID. */
  private Set<String> uniqueId;
  /** Holds the Unique ID of Collaboration objects and reference of them. */
  private Map<String, Collaboration> uniqueIdCollaboration;
  /** Holds the Unique ID of Organization objects and reference of them. */
  private Map<String, Organization> uniqueIdOrganization;
  /** Holds the Unique ID of Researcher objects and reference of them. */
  private Map<String, Researcher> uniqueIdResearcher;

  /** Holds the flag of saved or not. */
  private boolean saved = true;
  /** Final literal for commonly used string */
  public static final String COMMON = "> can not be found: ";
  /** Error Messages. */
  public String errorMessage = "";

  /**
   * Initializes this instance by reading in the three XML files.
   * 
   * @param collaborationsXml - path of Collaborations XML file.
   * @param organizationsXml - path of Organizations XML file.
   * @param researchersXml - path of Researchers XML file.
   * @throws JAXBException - Exception when marshalling.
   * @throws SAXException If JAXB throws an exception.
   */
  public XmlLoader(String collaborationsXml, String organizationsXml, String researchersXml)
      throws JAXBException, SAXException {
    this.collaborationsXml = collaborationsXml;
    this.organizationsXml = organizationsXml;
    this.researchersXml = researchersXml;
    // Load in the Collaborations XML example file.
    this.collaborationsJaxbContext = JAXBContext
        .newInstance("edu.hawaii.myisern.collaborations.jaxb");
    File collaborationsFile = new File(collaborationsXml);
    Unmarshaller unmarshaller = this.collaborationsJaxbContext.createUnmarshaller();
    /*
    // Validate XML
    SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
    Source schemaFile = new StreamSource(new File(COLLABORATIONS_XSD));
    Schema schema = factory.newSchema(schemaFile);
    unmarshaller.setSchema(schema);
    */
    this.collaborations = (Collaborations) unmarshaller.unmarshal(collaborationsFile);

    // Do the same for organizations.
    this.organizationsJaxbContext = JAXBContext
        .newInstance("edu.hawaii.myisern.organizations.jaxb");
    File organizationsFile = new File(organizationsXml);
    /*
    // Validate XML
    schemaFile = new StreamSource(new File(ORGANIZATIONS_XSD));
    schema = factory.newSchema(schemaFile);
    unmarshaller.setSchema(schema);
    */
    unmarshaller = this.organizationsJaxbContext.createUnmarshaller();

    this.organizations = (Organizations) unmarshaller.unmarshal(organizationsFile);

    // Now do it once more for the researchers.
    this.researchersJaxbContext = JAXBContext.newInstance("edu.hawaii.myisern.researchers.jaxb");
    File researchersFile = new File(researchersXml);
    /*
    // Validate XML
    schemaFile = new StreamSource(new File(RESEARCHERS_XSD));
    schema = factory.newSchema(schemaFile);
    unmarshaller.setSchema(schema);
    */
    unmarshaller = this.researchersJaxbContext.createUnmarshaller();
    this.researchers = (Researchers) unmarshaller.unmarshal(researchersFile);

    this.buildId();
    /* disable because overridable method is called during object construction.
     * Dont have idea of how to fix this. But the check is not necessary when contructing.
    // Check Links in objects by ID
    errorMessage = this.validate();
    if (errorMessage.length() > 0) {
      this.errorMessage = "Semantic errors are found when loading XML files: " + errorMessage;
    }
    */
  }

  /**
   * Constructor with parameter of folder name. Provide a convenient way to load a separate set of
   * XMLs.
   * 
   * @param folderName - String represent the folder name.
   * @throws JAXBException - Exception when marshalling.
   * @throws SAXException If JAXB throws an exception.
   */
  public XmlLoader(String folderName) throws JAXBException, SAXException {
    this(System.getProperty(USER_DIR) + "/xml/" + folderName + "/" + COLLABORATIONS_XML, System
        .getProperty(USER_DIR)
        + "/xml/" + folderName + "/" + ORGANIZATIONS_XML, System.getProperty(USER_DIR) + "/xml/"
        + folderName + "/" + RESEARCHERS_XML);
  } 

  /**
   * Check all collaborations in the Collaborations. Collaborating-Organizations should be refer to
   * a valid unique ID of an Organization. Years should be between 1990 and 2010.
   * 
   * @param collaborations - the Collaborations object that need to be checked.
   * @return - error message, empty if no errors..
   */
  private String semanticCheckAll(Collaborations collaborations) {
    String errorMessage = "";
    List<Collaboration> collaborationList = collaborations.getCollaboration();
    for (Collaboration collaboration : collaborationList) {
      if (!this.referenceCheck(collaboration)) {
        errorMessage += "There are invalid reference within " + collaboration.getName() + ".\n";
      }
      errorMessage += semanticCheck(collaboration);
    }
    return errorMessage;
  }

  /**
   * Check reference links in the given Collaboration. Collaborating-Organizations should be refer
   * to a valid unique ID of an Organization. Years should be between 1990 and 2010.
   * 
   * @param collaboration - the given collaboration
   * @return - error message, empty if no errors.
   */
  public String semanticCheck(Collaboration collaboration) {
    String errorMessage = "";
    // Check years.
    if (collaboration.getCollaboratingOrganizations().getCollaboratingOrganization().size() < 2) {
      errorMessage += "There must be at least 2 collaborating organizations.\n";
    }
    if (collaboration.getYears().getYear().size() < 1) {
      errorMessage += "There must be at least 1 year specified.\n";
    }
    else {
      List<BigInteger> years = collaboration.getYears().getYear();
      for (BigInteger year : years) {
        if (year.intValue() > 2010 || year.intValue() < 1990) {
          errorMessage += "Years in Collaboration " + collaboration.getName() + " is invalid: "
              + year + ". Should be between 1990 and 2010." + "\n";
        }
      }
    }
    if (collaboration.getCollaborationTypes().getCollaborationType().size() < 1) {
      errorMessage += "There must be at least 1 collaboration type.\n";
    }

    if (collaboration.getOutcomeTypes().getOutcomeType().size() < 1) {
      errorMessage += "There must be at least 1 outcome type.\n";
    }

    return errorMessage;
  }
  
  /**
   * Check reference within the given Collaboration.
   * @param collaboration the given collaboration.
   * @return true if all references are valid, otherwise false.
   */
  public boolean referenceCheck(Collaboration collaboration) {
    // Check organization reference.
    List<String> organizationList = collaboration.getCollaboratingOrganizations()
        .getCollaboratingOrganization();
    for (String organization : organizationList) {
      String id = toUniqueId(organization);
      if (!this.uniqueId.contains(id) || !this.uniqueIdOrganization.containsKey(id)) {
        return false;
      }
    }    
    return true;
  }
  
  /**
   * Check all organizations in the given Organizations. Contact cannot be empty, and should be
   * refer to a valid unique ID of a Researcher. Affiliated-Researcher should be refer to a valid
   * unique ID, but can be empty.
   * 
   * @param organizations - the Organizations object that need to be checked.
   * @return - error message, empty if no errors.
   */
  private String semanticCheckAll(Organizations organizations) {
    String errorMessage = "";
    List<Organization> organizationList = organizations.getOrganization();
    for (Organization organization : organizationList) {
      if (!this.referenceCheck(organization)) {
        errorMessage += "There are invalid reference within " + organization.getName() + ".\n";
      }
      errorMessage += semanticCheck(organization);
    }
    return errorMessage;
  }

  /**
   * Check reference links in the given Organization. Contact cannot be empty, and should be refer
   * to a valid unique ID of a Researcher. Affiliated-Researcher should be refer to a valid unique
   * ID, but can be empty.
   * 
   * @param organization - the given Organization.
   * @return - error message, empty if no errors.
   */
  public String semanticCheck(Organization organization) {
    String errorMessage = "";
    // check Contact.
    String contactId = toUniqueId(organization.getContact());
    if (contactId == null || !this.uniqueId.contains(contactId)
        || !this.uniqueIdResearcher.containsKey(contactId)) {
      errorMessage += "Valid contact is missing.\n";
    }
    return errorMessage;
  }

  /**
   * Check reference within the given Organization.
   * @param organization the given organization.
   * @return true if all references are valid, otherwise false.
   */
  public boolean referenceCheck(Organization organization) {
    // check Contact.
    String contactId = toUniqueId(organization.getContact());
    if (contactId == null || !this.uniqueId.contains(contactId)
        || !this.uniqueIdResearcher.containsKey(contactId)) {
      return false;
    }
    // check Affiliated-Researcher.
    List<String> researchers = organization.getAffiliatedResearchers().getAffiliatedResearcher();
    for (String researcher : researchers) {
      String id = toUniqueId(researcher);
      if (!this.uniqueId.contains(id) || !this.uniqueIdResearcher.containsKey(id)) {
        return false;
      }
    }
    return true;
  }

  /**
   * Check all researchers in the given Researchers. Organization should be refer to a valid unique
   * ID of an Organization.
   * 
   * @param researchers - the Researchers object that need to be checked.
   * @return - error message, empty if no errors.
   */
  private String semanticCheckAll(Researchers researchers) {
    String errorMessage = "";
    List<Researcher> researcherList = researchers.getResearcher();
    for (Researcher researcher : researcherList) {
      if (!this.referenceCheck(researcher)) {
        errorMessage += "There are invalid reference within " + researcher.getName() + ".\n";
      }
      errorMessage += semanticCheck(researcher);
    }
    return errorMessage;
  }

  /**
   * Check that the researcher should belong to an organization.
   * 
   * @param researcher - the given Researcher.
   * @return - error message, empty if no errors.
   */
  public String semanticCheck(Researcher researcher) {
    String errorMessage = "";
    String id = toUniqueId(researcher.getOrganization());
    if (id == null || !this.uniqueId.contains(id) || !this.uniqueIdOrganization.containsKey(id)) {
      errorMessage += "Researcher " + researcher.getName()
          + " does not have an valid Organization\n";
    }
    return errorMessage;
  }

  /**
   * Check reference links in the given Researcher. Organization should be refer to a valid unique
   * ID of an Organization.
   * 
   * @param researcher - the given Researcher.
   * @return - true if no error, otherwise false.
   */
  public boolean referenceCheck(Researcher researcher) {
    String id = toUniqueId(researcher.getOrganization());
    if (id == null || !this.uniqueId.contains(id) || !this.uniqueIdOrganization.containsKey(id)) {
      return false;
    }
    return true;
  }

  /**
   * Check inner structure of Collaboration.
   * 
   * @return - error message, empty if no errors.
   */
  private String checkCollaborationsStucture() {
    String errorMessage = "";
    for (String id : this.uniqueIdCollaboration.keySet()) {
      if (!this.uniqueId.contains(id)) {
        errorMessage += "Collaboration ID " + id + " is missing in ID set.\n";
      }
    }
    for (Collaboration collaboration : this.collaborations.getCollaboration()) {
      Collaboration temp = this.uniqueIdCollaboration.get(toUniqueId(collaboration.getName()));
      if (temp != collaboration) {
        errorMessage += "Collaboration " + collaboration.getName() + " is missing in ID map.\n";
      }
    }
    return errorMessage;
  }

  /**
   * Check inner structure of Organization.
   * 
   * @return - error message, empty if no errors.
   */
  private String checkOrganizationsStucture() {
    String errorMessage = "";
    for (String id : this.uniqueIdOrganization.keySet()) {
      if (!this.uniqueId.contains(id)) {
        errorMessage += "Organization ID " + id + " is missing in ID set.\n";
      }
    }
    for (Organization organization : this.organizations.getOrganization()) {
      Organization temp = this.uniqueIdOrganization.get(toUniqueId(organization.getName()));
      if (temp != organization) {
        errorMessage += "Organization " + organization.getName() + " is missing in ID map.\n";
      }
    }
    return errorMessage;
  }

  /**
   * Check inner structure of Researcher.
   * 
   * @return - error message, empty if no errors.
   */
  private String checkResearchersStucture() {
    String errorMessage = "";
    for (String id : this.uniqueIdResearcher.keySet()) {
      if (!this.uniqueId.contains(id)) {
        errorMessage += "Researcher ID " + id + " is missing in ID set.\n";
      }
    }
    for (Researcher researcher : this.researchers.getResearcher()) {
      Researcher temp = this.uniqueIdResearcher.get(toUniqueId(researcher.getName()));
      if (temp != researcher) {
        errorMessage += "Researcher " + researcher.getName() + " is missing in ID map.\n";
      }
    }
    return errorMessage;
  }

  /**
   * Check everything in this instance.
   * 
   * @return - false if any violation is found, otherwise, return true.
   */
  private String validate() {
    String errorMessage = this.checkCollaborationsStucture() + this.checkOrganizationsStucture()
        + this.checkResearchersStucture();
    if (errorMessage.length() > 0) {
      errorMessage = this.buildId();
    }
    return errorMessage + this.semanticCheckAll(collaborations)
        + this.semanticCheckAll(organizations) + this.semanticCheckAll(researchers);
  }

  /**
   * Build the ID set and ID map.
   * 
   * @return - True if build succeed
   */
  private String buildId() {
    String errorMessage = "";
    // Initialize the Unique ID
    uniqueId = new TreeSet<String>();
    // Add ID from Collaborations.
    uniqueIdCollaboration = new HashMap<String, Collaboration>();
    for (Collaboration collaboration : this.collaborations.getCollaboration()) {
      String id = toUniqueId(collaboration.getName());
      if (id == null || !uniqueId.add(id) || uniqueIdCollaboration.put(id, collaboration) != null) {
        errorMessage += "duplicated ID in Collaborations: " + id + "\n";
      }
    }
    // Add ID from Organizations.
    uniqueIdOrganization = new HashMap<String, Organization>();
    for (Organization organization : this.organizations.getOrganization()) {
      String id = toUniqueId(organization.getName());
      if (id == null || !uniqueId.add(id) || uniqueIdOrganization.put(id, organization) != null) {
        errorMessage += "duplicated ID in Organizations: " + id + "\n";
      }
    }
    // Add ID from Researchers.
    uniqueIdResearcher = new HashMap<String, Researcher>();
    for (Researcher researcher : this.researchers.getResearcher()) {
      String id = toUniqueId(researcher.getName());
      if (id == null || !uniqueId.add(id) || uniqueIdResearcher.put(id, researcher) != null) {
        errorMessage += "duplicated ID in Researchers: " + id + "\n";
      }
    }
    return errorMessage;
  }

  /**
   * Converts whitespace in string to underscore to meet the uniqueID format.
   * 
   * @param name A string containing whitespace.
   * @return uniqueID that has all underscores instead of whitespace.
   */
  public static String toUniqueId(String name) {
    if (name == null) {
      return null;
    }
    else {
      return name.replace(" ", "_");
    }
  }
  
  /**
   * Join Strings in a list into a single String.
   * Each value is divided by the given delimiting String.
   * 
   * @param list - the incoming List of String.
   * @param delimit - the given delimiting String.
   * @return - A String.
   */
  public static String joinListToString(List<String> list, String delimit) {
    String output = "";
    for (String value : list) {
      output += value + delimit;
    }
    output.split(output);
    return output;
  }
  
  /**
   * Join BigIntegers in a list into a single String.
   * Each value is divided by the given delimiting String.
   * 
   * @param list - the incoming List of BigInteger.
   * @param delimit - the given delimiting String.
   * @return - A String.
   */
  public static String joinBigIntegerListToString(List<BigInteger> list, String delimit) {
    String output = "";
    for (BigInteger value : list) {
      output += value.toString() + delimit;
    }
    output.split(output);
    return output;
  }
  
  /**
   * Split the String by the delimiting String and put the results in a List of String
   * 
   * @param input - the target String.
   * @param delimit - the delimiting String.
   * @return - A List of String.
   */
  public static List<String> splitStringToList(String input, String delimit) {
    List<String> list = new ArrayList<String>();
    if (input != null) {
      for (String value : input.split(delimit)) {
        list.add(value.trim());
      }
    }
    return list;
  }

  /**
   * Split the String by the delimiting String and put the results in a List of BigInteger
   * 
   * @param input - the target String.
   * @param delimit - the delimiting String.
   * @return - A List of BigInteger.
   */
  public static List<BigInteger> splitStringToBigIntegerList(String input, String delimit) {
    List<BigInteger> list = new ArrayList<BigInteger>();
    if (input != null) {
      for (String value : input.split(delimit)) {
        list.add(new BigInteger(value.trim()));
      }
    }
    return list;
  }
  
  /**
   * Return Collaborations that relative to a given researcher ID, null if not found.
   * 
   * @param id - String represent to the given researcher ID.
   * @return - Collaborations object.
   */
  public Collaborations getCollaborationsByResearcher(String id) {
    String organization = null;
    if (this.uniqueId.contains(id) && this.uniqueIdResearcher.containsKey(id)) {
      organization = toUniqueId(this.uniqueIdResearcher.get(id).getOrganization());
      return this.getCollaborationsByOrganization(organization);
    }
    else {
      return null;
    }
  }

  /**
   * Return Collaborations that relative to a given organization ID, null if not found.
   * 
   * @param id - String represent to the given organization ID.
   * @return - Collaborations object.
   */
  public Collaborations getCollaborationsByOrganization(String id) {
    Collaborations collaborations = new Collaborations();
    if (this.uniqueId.contains(id) && this.uniqueIdOrganization.containsKey(id)) {
      for (Collaboration collaboration : this.collaborations.getCollaboration()) {
        for (String organization : collaboration.getCollaboratingOrganizations()
            .getCollaboratingOrganization()) {
          if (id.equals(toUniqueId(organization))) {
            collaborations.getCollaboration().add(collaboration);
          }
        }
      }
    }
    if (collaborations.getCollaboration().isEmpty()) {
      collaborations = null;
    }
    return collaborations;
  }

  /**
   * Return Collaborations that relative to a given Year, null if not found.
   * 
   * @param id - int represent to the given Year.
   * @return - Collaborations object.
   */
  public Collaborations getCollaborationsByYear(int id) {
    Collaborations collaborations = new Collaborations();
    for (Collaboration collaboration : this.collaborations.getCollaboration()) {
      for (BigInteger year : collaboration.getYears().getYear()) {
        if (year.intValue() == id) {
          collaborations.getCollaboration().add(collaboration);
        }
      }
    }
    if (collaborations.getCollaboration().isEmpty()) {
      collaborations = null;
    }
    return collaborations;
  }

  /**
   * Return all collaborations within this instance.
   * 
   * @return - a Collaborations object that contain all collaborations
   */
  public Collaborations getCollaborations() {
    return this.collaborations;
  }

  /**
   * Return the collaboration that is identified by the given ID.
   * 
   * @param id - String that represent the collaboration ID, return null if not found.
   * @return - Collaborations contains the specified Collaboration. null if not found.
   */
  public Collaboration getCollaboration(String id) {
    return this.uniqueIdCollaboration.get(id);
  }

  /**
   * Return all organizations within this instance.
   * 
   * @return - Organizations object contains all organizations
   */
  public Organizations getOrganizations() {
    return this.organizations;
  }

  /**
   * Return the organization that is identified by the given ID.
   * 
   * @param id - String that represent the organization ID, return null if not found.
   * @return - Organizations object contains the specified Organization. null if not found.
   */
  public Organization getOrganization(String id) {
    return this.uniqueIdOrganization.get(id);
  }

  /**
   * Return all researchers within this instance.
   * 
   * @return - Researchers object contains all researchers.
   */
  public Researchers getResearchers() {
    return this.researchers;
  }

  /**
   * Return the researcher that is identified by the given ID.
   * 
   * @param id - String that represent the researcher ID, return null if not found.
   * @return - Researchers object contains the specified Researcher. null if not found.
   */
  public Researcher getResearcher(String id) {
    return this.uniqueIdResearcher.get(id);
  }

  /**
   * Return a List of unique ID that represent organizations that engaged in more collaborations
   * than the specified number.
   * 
   * @param i - an int of the specified number
   * @return - List of String contains the unique IDs.
   */
  public List<String> getOrganizationCollaborateGreaterThan(int i) {
    List<String> idList = new ArrayList<String>();
    Map<String, Integer> count = getNumberOfCollaborations();
    for (String id : count.keySet()) {
      if (count.get(id).compareTo(i) > 0) {
        idList.add(id);
      }
    }
    return idList;
  }

  /**
   * Return a List of unique ID that represent organizations that engaged in the specified number of
   * collaborations.
   * 
   * @param i - an int of the specified number
   * @return - List of String contains the unique IDs.
   */
  public List<String> getOrganizationCollaborateEquals(int i) {
    List<String> idList = new ArrayList<String>();
    Map<String, Integer> count = getNumberOfCollaborations();
    for (String id : count.keySet()) {
      if (count.get(id).equals(i)) {
        idList.add(id);
      }
    }
    return idList;
  }

  /**
   * Return the number of engaged collaborations of each organization.
   * 
   * @return - A Map, which key holds the unique ID of organizations and value holds the count.
   */
  public Map<String, Integer> getNumberOfCollaborations() {
    Map<String, Integer> count = new HashMap<String, Integer>();
    // Initialize with 0
    for (String id : this.uniqueIdOrganization.keySet()) {
      count.put(id, 0);
    }
    for (Collaboration collaboration : this.collaborations.getCollaboration()) {
      List<String> organizations = collaboration.getCollaboratingOrganizations()
          .getCollaboratingOrganization();
      for (String organization : organizations) {
        String id = toUniqueId(organization);
        count.put(id, count.get(id) + 1);
      }
    }
    return count;
  }

  /**
   * Add the given Collaboration object into the system.
   * 
   * @param collaboration - the given Collaboration object.
   * @return - the old Collaboration object with the same ID with the given one. Null if the ID does
   * not exist yet.
   */
  public Collaboration add(Collaboration collaboration) {
    if (collaboration.getName() == null) {
      this.errorMessage += "Can not add collaboration with no name.\n";
      return null;
    }
    String id = toUniqueId(collaboration.getName());
    saved = false;
    if (this.uniqueId.add(id)) {
      this.collaborations.getCollaboration().add(collaboration);
      return this.uniqueIdCollaboration.put(id, collaboration);
    }
    else {
      this.collaborations.getCollaboration().add(collaboration);
      Collaboration old = this.uniqueIdCollaboration.put(id, collaboration);
      this.collaborations.getCollaboration().remove(old);
      return old;
    }
  }

  /**
   * Add the given Organization object into the system.
   * 
   * @param organization - the given Organization object.
   * @return - the old Organization object with the same ID with the given one. Null if the ID does
   * not exist yet.
   */
  public Organization add(Organization organization) {
    if (organization.getName() == null) {
      this.errorMessage += "Can not add organization with no name.\n";
      return null;
    }
    String id = toUniqueId(organization.getName());
    saved = false;
    if (this.uniqueId.add(id)) {
      this.organizations.getOrganization().add(organization);
      return this.uniqueIdOrganization.put(id, organization);
    }
    else {
      this.organizations.getOrganization().add(organization);
      Organization old = this.uniqueIdOrganization.put(id, organization);
      this.organizations.getOrganization().remove(old);
      return old;
    }
  }

  /**
   * Add the given Researcher object into the system.
   * 
   * @param researcher - the given Researcher object.
   * @return - the old Researcher object with the same ID with the given one. Null if the ID does
   * not exist yet.
   */
  public Researcher add(Researcher researcher) {
    if (researcher.getName() == null) {
      this.errorMessage += "Can not add reseacher with no name.\n";
      return null;
    }
    String id = toUniqueId(researcher.getName());
    saved = false;
    if (this.uniqueId.add(id)) {
      this.researchers.getResearcher().add(researcher);
      return this.uniqueIdResearcher.put(id, researcher);
    }
    else {
      this.researchers.getResearcher().add(researcher);
      Researcher old = this.uniqueIdResearcher.put(id, researcher);
      this.researchers.getResearcher().remove(old);
      return old;
    }
  }

  /**
   * Remove the given id and its associated object.
   * 
   * @param id - String of the ID
   * @return - true if the id and its associated object is successfully removed. otherwise, false is
   * returned.
   */
  public boolean remove(String id) {
    if (this.uniqueId.contains(id)) {
      this.uniqueId.remove(id);
      if (this.uniqueIdCollaboration.containsKey(id)) {
        Collaboration removedCollaboration = this.uniqueIdCollaboration.remove(id);
        if (removedCollaboration != null
            && this.collaborations.getCollaboration().remove(removedCollaboration)) {
          System.out.println("Collaboration " + id + " is removed.");
          saved = false;
          return true;
        }
        else {
          System.err.println("Error: Collaboration ID " + id
              + " is not correct match to a collaboration, please check the system.");
          return false;
        }
      }
      else if (this.uniqueIdOrganization.containsKey(id)) {
        Organization removedOrganization = this.uniqueIdOrganization.remove(id);
        if (removedOrganization != null
            && this.organizations.getOrganization().remove(removedOrganization)) {
          System.out.println("Organization " + id + " is removed.");
          saved = false;
          return true;
        }
        else {
          System.err.println("Error: Organization ID " + id
              + " is not correct match to an organization, please check the system.");
          return false;
        }
      }
      else if (this.uniqueIdResearcher.containsKey(id)) {
        Researcher removedResearcher = this.uniqueIdResearcher.remove(id);
        if (removedResearcher != null 
            && this.researchers.getResearcher().remove(removedResearcher)) {
          System.out.println("Researhcer " + id + " is removed.");
          saved = false;
          return true;
        }
        else {
          System.err.println("Error: Researcher ID " + id
              + " is not correct match to a researcher, please check the system.");
          return false;
        }
      }
      else {
        System.err.println("Error: ID " + id
            + " is not correct match to any object, please check the system.");
        return false;
      }
    }
    else {
      System.out.println("ID " + id + " not found, please check the spelling.");
      return false;
    }
  }

  /**
   * Save the system into 3 XML files.
   * 
   * @return - null if saving completed successfully, otherwise, error messages are returned.
   * @throws JAXBException - Exception when marshalling.
   * @throws FileNotFoundException - Problems when opening file.
   * @throws SAXException If JAXB throws an exception.
   */
  public String save() throws JAXBException, FileNotFoundException, SAXException {
    errorMessage = this.validate();
    if (errorMessage.length() == 0) {

      // save Collaborations
      Marshaller marshaller = collaborationsJaxbContext.createMarshaller();
      marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
      OutputStream outputStream = new FileOutputStream(collaborationsXml);
      /*
      // Validate XML
      SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
      Source schemaFile = new StreamSource(new File(COLLABORATIONS_XSD));
      Schema schema = factory.newSchema(schemaFile);
      marshaller.setSchema(schema);
      */
      marshaller.marshal(this.collaborations, outputStream);
      // save Organizations
      marshaller = organizationsJaxbContext.createMarshaller();
      marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
      outputStream = new FileOutputStream(organizationsXml);
      /*
      // Validate XML
      schemaFile = new StreamSource(new File(ORGANIZATIONS_XSD));
      schema = factory.newSchema(schemaFile);
      marshaller.setSchema(schema);
      */
      marshaller.marshal(this.organizations, outputStream);
      // save Researchers
      marshaller = researchersJaxbContext.createMarshaller();
      marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
      outputStream = new FileOutputStream(researchersXml);
      /*
      // Validate XML
      schemaFile = new StreamSource(new File(RESEARCHERS_XSD));
      schema = factory.newSchema(schemaFile);
      marshaller.setSchema(schema);
      */
      marshaller.marshal(this.researchers, outputStream);
      this.saved = true;
    }
    return errorMessage;
  }

  /**
   * Return the List of name of Collaborations.
   * 
   * @return - the List of String that contains names.
   */
  public List<String> getCollaborationNames() {
    List<String> nameList = new ArrayList<String>();
    for (Collaboration collaboration : this.collaborations.getCollaboration()) {
      nameList.add(collaboration.getName());
    }
    return nameList;
  }

  /**
   * Return the List of name of Organizations.
   * 
   * @return - the List of String that contains names.
   */
  public List<String> getOrganizationNames() {
    List<String> nameList = new ArrayList<String>();
    for (Organization organization : this.organizations.getOrganization()) {
      nameList.add(organization.getName());
    }
    return nameList;
  }

  /**
   * Return the List of name of Researchers.
   * 
   * @return - the List of String that contains names.
   */
  public List<String> getResearcherNames() {
    List<String> nameList = new ArrayList<String>();
    for (Researcher researcher : this.researchers.getResearcher()) {
      nameList.add(researcher.getName());
    }
    return nameList;
  }

  /**
   * Return true if the given name of Collaboration is contained in the system.
   * 
   * @param name - String of the given name.
   * @return - true if the name is in the system.
   */
  public boolean hasCollaboration(String name) {
    /*
    if (!this.collaborations.getCollaboration().contains(this.uniqueIdCollaboration.get(name))) {
      System.out.println("Fixed a invalid link.");
      this.uniqueIdCollaboration.remove(name);
    }
    */
    return this.uniqueIdCollaboration.containsKey(toUniqueId(name));
  }

  /**
   * Return true if the given name of Organization is contained in the system.
   * 
   * @param name - String of the given name.
   * @return - true if the name is in the system.
   */
  public boolean hasOrganization(String name) {
    return this.uniqueIdOrganization.containsKey(toUniqueId(name));
  }

  /**
   * Return true if the given name of Researcher is contained in the system.
   * 
   * @param name - String of the given name.
   * @return - true if the name is in the system.
   */
  public boolean hasResearcher(String name) {
    return this.uniqueIdResearcher.containsKey(toUniqueId(name));
  }

  /**
   * Return the saved flag.
   * 
   * @return - true if system has been saved, otherwise, false.
   */
  public boolean isSaved() {
    return saved;
  }
}